/*
 * This file is part of the Boring Engine.
 *
 * Copyright (c) 2011 by pf5234 <pf5234@users.sourceforge.net>.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

package com.googlecode.boringengine.lua;

import com.googlecode.boringengine.Log;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.luaj.vm.LBoolean;
import org.luaj.vm.LDouble;
import org.luaj.vm.LFunction;
import org.luaj.vm.LInteger;
import org.luaj.vm.LNil;
import org.luaj.vm.LString;
import org.luaj.vm.LTable;
import org.luaj.vm.LUserData;
import org.luaj.vm.LValue;
import org.luaj.vm.LuaState;

class Wrapper extends LFunction {

    private Method theMeth;
    private Object theObject;

    private Wrapper(Method meth, Object obj) {
        theMeth = meth;
        theObject = obj;
    }

    public static void load(LTable globals, Class<?> cl, String table, Object obj) {
        LTable methodTable = table.equals("") ? globals : new LTable();
        for (Method meth : cl.getMethods()) {
            if (! meth.isAnnotationPresent(LuaFunction.class))
                continue;
            //Log.debug("@LuaFunction %s.%s()", cl.getName(), meth.getName());
            methodTable.put(meth.getName(), new Wrapper(meth, obj));
        }
        methodTable.put("___object", new LUserData(obj));
        if (globals != methodTable)
            globals.put(table, methodTable);
    }

    @Override
    public int invoke(LuaState lua) {
        try {
            boolean varargs = theMeth.isVarArgs();
            Class<?>[] classes = theMeth.getParameterTypes();
            int length = varargs ? Math.max(classes.length - 1, lua.gettop())
                    : classes.length;
            Object[] objs = new Object[classes.length];
            Object vararg = null;
            if (varargs) {
                vararg = Array.newInstance(classes[classes.length - 1].getComponentType(),
                        lua.gettop() - classes.length + 1);
            }
            for (int i = 0; i < length; i++) {
                Class<?> cl;
                Object obj;
                if (varargs && i >= classes.length - 1)
                    cl = classes[classes.length - 1].getComponentType();
                else
                    cl = classes[i];
                LValue val = lua.checkany(i + 1);
                if (cl.equals(int.class) || cl.equals(Integer.class)) {
                    obj = val.toJavaInt();
                } else if (cl.equals(double.class) || cl.equals(Double.class)) {
                    obj = val.toJavaDouble();
                } else if (cl.equals(boolean.class) || cl.equals(Boolean.class)) {
                    obj = val.toJavaBoolean();
                } else if (cl.equals(String.class)) {
                    obj = val.toJavaString();
                } else if (cl.equals(LValue.class)) {
                    obj = val;
                } else if (val instanceof LTable) {
                    obj = LuaInterpreter.getObject(val);
                } else {
                    Log.warn("Unknown class %s, param %d null", cl, i);
                    obj = null;
                }
                if (varargs && i >= classes.length - 1)
                    Array.set(vararg, i - classes.length + 1, obj);
                else
                    objs[i] = obj;
            }
            if (varargs)
                objs[objs.length - 1] = vararg;
            Object returned = theMeth.invoke(theObject.getClass().cast(theObject), objs);
            Class<?> cl = theMeth.getReturnType();
            if (cl.equals(objs.getClass())) {
                Object[] ret = (Object[]) returned;
                for (Object o : ret) {
                    LValue magic = null;
                    if (o == null) {
                        magic = LNil.NIL;
                    } else if (o instanceof Integer) {
                        magic = LInteger.valueOf((Integer)o);
                    } else if (o instanceof Double) {
                        magic = LDouble.valueOf((Double)o);
                    } else if (o instanceof Boolean) {
                        magic = LBoolean.valueOf((Boolean)o);
                    } else if (o instanceof String) {
                        magic = LString.valueOf((String)o);
                    } else {
                        magic = LuaInterpreter.convertObj(o);
                    }
                    lua.pushlvalue(magic);
                }
                return ret.length;
            } else if (cl.equals(int.class) || cl.equals(Integer.class)) {
                lua.pushinteger((Integer) returned);
                return 1;
            } else if (cl.equals(double.class) || cl.equals(Double.class)) {
                lua.pushnumber((Double) returned);
                return 1;
            } else if (cl.equals(boolean.class) || cl.equals(Boolean.class)) {
                lua.pushboolean((Boolean) returned);
                return 1;
            } else if (cl.equals(String.class)) {
                lua.pushstring((String) returned);
                return 1;
            } else if (cl.equals(void.class) || cl.equals(Void.class)) {
                return 0;
            } else {
                LValue object = LuaInterpreter.convertObj(returned);
                lua.pushlvalue(object);
                return 1;
            }
        } catch (InvocationTargetException e) {
            Throwable ex = e.getCause();
            Log.error("Error in %s.%s: %s", theObject.getClass().getName(),
                    theMeth.getName(), ex);
        } catch (Exception ex) {
            Log.error("Couldn't call %s: %s (%s)", theMeth.getName(), ex, ex.getCause());
        }
        return 0;
    }

    @Override
    public String toJavaString() { return theMeth.getName() + "()"; }

}
